﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using Stauffware.Common.Core;

namespace Stauffware.Common.Console
{
	/// <summary>
	/// A TextWriter filter that performs basic formatting of plain monospaced text.
	/// </summary>
	public class FormattingTextWriter : TextWriter
	{
		#region Public Fields & Properties

		/// <summary>
		/// Gets the TextWriter to which this object writes its output.
		/// </summary>
		public readonly TextWriter Output;

		/// <summary>
		/// Gets or sets the maximum number of characters per line.  
		/// This limit includes any prefixes or suffixes.
		/// </summary>
		public uint LineWidth
		{
			get { return this._lineWidth; }
			
			set
			{
				if (value > this.Buffer.Length)
				{
					char[] oldBuf = this.Buffer;
					this.Buffer = new char[value * 2];
					oldBuf.CopyTo(this.Buffer, 0);
				}

				this._lineWidth = value;
				this.CheckProperties();
			}
		}
		private uint _lineWidth;

		/// <summary>
		/// Gets or sets a string to be prepended to every line.
		/// Characters &lt; 0x20 are not allowed, except for horizontal tab (0x09).
		/// </summary>
		public string LinePrefix
		{
			get { return this._linePrefix; }
			set 
			{ 
				this._linePrefix = (value == null) ? "" : value;
				this.CheckProperties();
			}
		}
		private string _linePrefix = "";

		/// <summary>
		/// Gets or sets a string to be appended to every line.
		/// Characters  &lt; 0x20 are not allowed, except for horizontal tab (0x09).
		/// </summary>
		public string LineSuffix
		{
			get { return this._lineSuffix; }
			set 
			{
				Require.Condition(string.IsNullOrEmpty(value), "LineSuffix property is not implemented yet");
				this._lineSuffix = (value == null) ? "" : value;
				this.CheckProperties();
			}
		}
		private string _lineSuffix = "";

		///// <summary>
		///// Gets or sets the number of spaces by which to indent the first line.
		///// min. value = - LinePrefix.Length
		///// </summary>
		//public int FirstLineIndent
		//{
		//    get { return this._firstLineIndent; }
		//    set
		//    {
		//        Require.Condition(value == 0, "FirstLineIndent property is not implemented yet");
		//        this._firstLineIndent = value;
		//        this.CheckProperties();
		//    }
		//}
		//private int _firstLineIndent = 0;

		/// <summary>
		/// Gets or sets the width of a tab stop width, in characters.
		/// </summary>
		public uint TabWidth = 4;

		/// <summary>
		/// Gets or sets a string to be appended to each line that wraps to the next line,
		/// just before the line break.  Example: "…" (elipsis).
		/// </summary>
		public string PreLineWrap
		{
			get { return this._preLineWrap; }
			set
			{
				Require.Condition(string.IsNullOrEmpty(value), "LineWrapSuffix property is not implemented yet");
				this._preLineWrap = (value == null) ? "" : value;
				this.CheckProperties();
			}
		}
		private string _preLineWrap = "";

		/// <summary>
		/// Gets or sets a string to be prepended to each line that wrapped from the previous line
		/// just after the line break.  Example: "…" (elipsis).
		/// </summary>
		public string PostLineWrap
		{
			get { return this._postLineWrap; }
			set 
			{
				this._postLineWrap = (value == null) ? "" : value;
				this.CheckProperties();
			}
		}
		private string _postLineWrap = "";

		///// <summary>
		///// whether to right-align continuation lines
		///// </summary>
		//public bool RightAlignWrapped = false;

		/// <summary>
		/// Gets the number of physical lines written so far.
		/// </summary>
		public uint LineCount { get; private set; }

		/// <summary>
		/// Gets the next column that will receive a character.
		/// </summary>
		public uint NextColumn { get; private set; }

		/// <inherit/>
		public override Encoding Encoding { get { return this.Output.Encoding; } }

		#endregion

		/// <summary>
		/// Specifies how a character will be handled.
		/// </summary>
		private enum CharType
		{
			Ignore,			// character is ignored
			Normal,
			Alias,			// character is replaced with a different character
			NewLine,		// character is not emitted; a new line is begun
			Space,			// character is a (single) space
			Tab,			// character is a horizontal tab
			BreakBefore,
			BreakAfter,
			BreakAndDiscard,
			SoftHyphen		
		}

		private char LastInputChar = '\0';
		private char LastCharWritten = '\0';

		private char[] Buffer;
		private uint CharsInBuffer = 0;

		/// <summary>
		/// Returns true if we're at the beginning of a new empty line, 
		/// with nothing buffered.
		/// </summary>
		protected bool AtStartOfEmptyLine
		{ get { return (this.NextColumn == 0) && (this.CharsInBuffer == 0); } }

		/// <summary>
		/// True if the current line is a wrapped continuation of the previous line
		/// (not counting anything that might be in the buffer).
		/// </summary>
		protected bool OnWrappedLine;

		//// KLUDGE ALERT: this is a bandaid on a bug:
		//// if a line exactly fills up all the available columns, then we mustn't
		//// write a newline, because the line has already wrapped.
		//private uint SafeLineWidth { get { return this.LineWidth - 1; } }

		// for debugging
		private string StringInBuffer { get { return new string(this.Buffer, 0, (int) this.CharsInBuffer); } }

		private readonly char[] BreakBeforeChars = 
		{ '[', '(' };

		private readonly char[] BreakAfterChars = 
		{ ':', ';', '&', '|', '/', '\\', Chars.EnDash.Unicode, Chars.EmDash.Unicode };

		// The minimum number of "real" characters per line (not including prefixes & suffixes)
		private const int MIN_EFFECTIVE_LINE_WIDTH = 3;

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="output">where to write the formatted output</param>
		/// <param name="lineWidth">maximum chars per line</param>

		public FormattingTextWriter (TextWriter output, uint lineWidth)
		{
			Require.NotNull(output, "output must not be null");

			this.Output = output;
			this.Buffer = new char[lineWidth * 2];
			this._lineWidth = lineWidth;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Writes one character.
		/// </summary>

		public override void Write (char c)
		{
			CharType chType = this.GetType(c);
			this.LastInputChar = c;

			switch (chType)
			{
				case CharType.BreakAfter:
					this.AppendBuffer(c);
					this.WriteBuffer();
					break;

				case CharType.BreakAndDiscard:
					this.WriteBuffer();
					// drop c on the floor
					break;

				case CharType.BreakBefore:
					this.WriteBuffer();
					this.AppendBuffer(c);
					break;

				case CharType.Ignore:
					// drop c on the floor
					break;

				case CharType.NewLine:
					this.WriteBuffer();
					this.WriteNewLine();
					this.OnWrappedLine = false;
					break;

				case CharType.Normal:
					this.AppendBuffer(c);
					break;

				case CharType.SoftHyphen:
					Assert.NotImplemented();
					break;

				case CharType.Space:
					this.WriteBuffer();

					// Drop spaces at the beginning of continuation lines.
					//if (!(this.AtStartOfEmptyLine && this.OnWrappedLine))
					this.AppendBuffer(' ');
					break;

				case CharType.Tab:
					this.WriteBuffer();
					uint nextTabCol = ((this.NextColumn + this.TabWidth) / this.TabWidth) * this.TabWidth;
		// WRONG
					if (nextTabCol < this.NextColumn)
						this.WriteNewLine();
					this.WriteOutput("".PadRight((int)(nextTabCol - this.NextColumn)));
					break;

				default:
					Assert.NoCaseFor(chType); break;
			}
		}

		//-----------------------------------------------------------------------------
		/// <inherit/>

		public override void Flush ( )
		{
			this.WriteBuffer();
			base.Flush();
		}

		//-----------------------------------------------------------------------------
		/// <inherit/>

		public override void Close ( )
		{
			this.Flush();
			this.Output.Close();
		}

		//-----------------------------------------------------------------------------

		private CharType GetType (char c)
		{
			if (c == Chars.SoftHyphen.Unicode) return CharType.SoftHyphen;

			if (c == '\r') return CharType.NewLine;
			if (c == '\n') return (this.LastInputChar == '\r') ? CharType.Ignore : CharType.NewLine;
			if (c == '\t') return CharType.Tab;
			if (c == Chars.NonBreakingSpace) return CharType.Normal;
			if (c == Chars.ZeroWidthNonJoiner) return CharType.BreakAndDiscard;
			if (char.IsWhiteSpace(c)) return CharType.Space;

			if (Array.IndexOf(this.BreakAfterChars, c) >= 0) return CharType.BreakAfter;
			if (Array.IndexOf(this.BreakBeforeChars, c) >= 0) return CharType.BreakBefore;

			// NOTE: ideally, the following characters depend on context:
			//		'.'		decimal point (Normal) vs. name delimiter (BreakAfter)
			//		','		digit separator (Normal) vs. list delimiter (BreakAfter)
			//		'-'		sign of number (Normal) vs. compound word (BreakAfter)

			if (c < ' ') return CharType.Ignore;

			return CharType.Normal;
		}

		//-----------------------------------------------------------------------------

		private void AppendBuffer (char c)
		{
			this.Buffer[this.CharsInBuffer++] = c;

			// check for buffer overflow
			if (this.CharsInBuffer > this.LineWidth)
			{
				// write as many chars from the buffer as will fit on the current line

				Assert.NotImplemented();

				this.WriteNewLine();
			}
		}

		//-----------------------------------------------------------------------------

		private void WriteBuffer ( )
		{
			uint colsRemaining = this.LineWidth - this.NextColumn;

			uint leadingSpaces = 0;

			while (leadingSpaces < this.CharsInBuffer && this.Buffer[leadingSpaces] == ' ')
				leadingSpaces++;

			uint charsToWrite;

			if (this.CharsInBuffer > colsRemaining)
			{
				this.WriteNewLine();
				this.OnWrappedLine = true;
				// drop leading spaces at beginning of continuation line
				charsToWrite = this.CharsInBuffer - leadingSpaces;
			}
			else
			{
				charsToWrite = this.CharsInBuffer;
				leadingSpaces = 0;
			}

			if (charsToWrite == 0)
				return;

			if (this.AtStartOfEmptyLine)
			{
				if (this.OnWrappedLine)
					this.WriteOutput(this.LinePrefix);
				else this.WriteOutput(this.PostLineWrap);
			}

			this.Output.Write(this.Buffer, (int) leadingSpaces, (int) charsToWrite);
			this.LastCharWritten = this.Buffer[leadingSpaces + charsToWrite - 1];
			this.NextColumn += charsToWrite;
			this.CharsInBuffer = 0;
		}

		//-----------------------------------------------------------------------------

		private void WriteNewLine ( )
		{
			this.Output.Write('\n');
			this.LineCount++;
			this.NextColumn = 0;		// WRONG
		}

		//-----------------------------------------------------------------------------

		private void WriteOutput (char c)
		{
			this.Output.Write(c);
			this.NextColumn++;
		//	this.LastCharWritten = c;
		}

		//-----------------------------------------------------------------------------

		private void WriteOutput (string s)
		{
			if (s.Length == 0) return;
			this.Output.Write(s);
			this.NextColumn += (uint) s.Length;
		//	this.LastCharWritten = s[s.Length - 1];
		}

		//-----------------------------------------------------------------------------

		private void CheckProperties ( )
		{
			int overhead = this.LinePrefix.Length + this.PostLineWrap.Length +
						   this.LineSuffix.Length + this.PreLineWrap.Length + 
						   MIN_EFFECTIVE_LINE_WIDTH;

			//if (this.FirstLineIndent > 0)
			//    overhead += this.FirstLineIndent;

			if (overhead > this.LineWidth)
				throw new InputRequirement("the total lengths of the prefixes and suffixes must be at least " + 
									MIN_EFFECTIVE_LINE_WIDTH.ToString() + " less than LineWidth.");

			//if (this.FirstLineIndent < 0 && this.LinePrefix.Length < -this.FirstLineIndent)
			//    throw new InputRequirement("LinePrefix length must not be less than (- FirstLineIndent)");
		}

		//-----------------------------------------------------------------------------
	}
}
